6W - 이스티오 컨트롤 플레인 성능 최적화
개요
대규모의 클러스터를 운영하다보면 자연스레 성능 문제를 신경 쓸 수밖에 없다.
컨트롤 플레인이 데이터 플레인의 모든 리소스와 통신을 해야 하기 때문에, 컨트롤 플레인 부하를 많이 받을 가능성을 항상 내포하고 있으며, 최적화에 있어서도 중요한 고려요소가 된다.
동기화 구조
동기화가 제때에 되지 않아 에러가 발생할 수 있다고 했다.
컨트롤 플레인은 결국 사용자가 지정한 희망 상태에 맞춰 데이터 플레인의 현재 상태를 계속 맞추어나가는 프로세스이다.
이를 위해 컨트롤 플레인은 지속적으로 데이터 플레인의 상태를 모니터링하며, 이러한 방식으로 인해 즉각적으로 사용자의 설정이 반영되지 않을 수 있다.
대신 궁극적 일관성을 지향하기 때문에 결국엔 동기화가 이뤄질 것이다.
관리자가 이스티오 설정을 컨트롤 플레인에 전달한 직후에는 당연히 짧게 동기화가 되지 않는 순간이 있을 것이다.
그러나 이것 말고도 동기화가 유지되지 않을 수 있는 대표적인 순간은 메시 내의 워크로드, 서비스에 변화가 생기는 순간이다.
당연한 상식을 먼저 짚자면,
- 노드의 kubelet은 자신이 관리하는 워크로드의 헬스체크를 수행하고 이를 kube-apiserver로 전송한다.
- istiod는 api서버와 통신한다.
- 이스티오 관련 리소스가 생성될 때, 관련한 정보를 얻기 위해 istiod가 api 서버와 통신하는 건 너무도 당연하다.
istiod는 이스티오 리소스가 생성될 때만 api서버와 통신하는 것이 아니다.
런타임, 버전 정보 등 다양한 설정과 클러스터 환경에서 대해 istiod는 api 서버로부터 정보를 얻어 활용한다.
서비스 B-2가 죽은 상황을 생각해보자.
istiod는 모든 설정을 동기화시켜주고 있기 때문에, B-2가 죽으면 언젠가 동기화가 되지 않는 순간을 포착하고 문제가 생겼음을 알 수 있을 것이다.
하지만 동기화는 기본적으로 설정이 변경됐을 때 수행하는 작업이기에, istiod는 실제 서비스의 상태 여부를 판단할 때 kube-apiserver를 활용한다.
(이 헬스체크는 엔보이들이 개별적으로 행하는 이상치 탐지와 관련 없다.)
이 정보를 토대로 istiod는 B-2를 클러스터로 삼고 있는 A에게 이를 알려줘야 한다.
그래야 이미 죽어버린 워크로드에게 트래픽을 보내는 장애 상황이 발생하지 않을 것이기 때문이다.
그럼에도 위 상황에서 istiod가 A에게 해당 설정을 전파하기 전에 A가 B-2로 트래픽을 보내는 순간이 있을 수 있다.
이렇게 이미 죽었지만 트래픽을 받는 워크로드를 유령 워크로드(phantom workload)라 부르며, 이는 장애 상황 유발 및 성능 저하를 일으킨다.
이런 상황을 어떻게 대처할 수 있는가?
이스티오의 기본 활용법으로 치자면,
- 재시도, 타임아웃 설정
- 이상치 탐지 설정
등으로 대응할 수 있다.
어차피 궁극적 일관성이 충족되니 언젠가 istiod는 유령 워크 문제를 해결할 것이다.
여기에서 개선하고자 하는 것은 바로 이 동기화가 일어나는 속도이다.
워크로드와 서비스가 어마무시하게 많은 메시 환경에서는 이 동기화에도 시간이 굉장히 오래 걸릴 수 있다.
이걸 해결해야 한다!
데이터 플레인 동기화 단계
동기화 작업을 할 때, istiod는 프로세스 상 크게 두 가지 성능 최적화 기법을 적용한다. - DiscoveryServer가 동기화할 이벤트를 수신하고 **디바운스(Debounce)** - 실시간으로 작업하지 않고 이벤트를 모아 그룹화시켜 처리하는 동작을 말한다. - 여러 동작을 한꺼번에, 효율적으로 처리하여 자원 소모를 줄이고 성능을 향상시키는 것이다. - 병합된 이벤트를 작업 큐에 넣고, 개수 단위로 **쓰로틀(Throttle)** 하여 푸시 - 이벤트를 1)엔보이 설정으로 변환하고, 2)푸시하는 작업을 해야 한다. - 이때 이미 처리되는 항목을 더 빨리 처리되도록 보장하면서, 컨텍스트 스위칭을 줄인다.이벤트들은 api 서버로부터 받아오는 것들이고, 사용자의 설정이든 클러스터 단에서의 변화든 다양한 이벤트가 발생할 수 있을 것이다.
일차적으로는 이것들을 그룹화시키는 디바운싱 작업을 거쳐서 작업 큐에 넣고, 이후에는 개수 단위로 배치 처리하는 식으로 동기화가 일어난다.
여기에서 컨트롤 플레인의 작업에 영향을 주는 요소들은 다음과 같다.
- 변경 속도 - 변경 속도가 빠를수록 데이터 플레인을 동기화하기 위해 더 많이 처리
- 할당된 리소스 - 프로세스는 당연히 컴퓨팅 자원에 영향 받음..
- 업데이트할 워크로드 개수 - 업데이트할 워크로드가 많을 수록 네트워크 대역폭과 처리 능력 필요
- 설정 크기 - 엔보이 설정 파일이 클 수록 처리 능력과 네트워크 대역폭이 필요
메시의 규모가 커지면 할당 리소스를 제외한 요소들이 더욱 많은 부하를 일으키게 될 것이다.
이 각 요소에 대해 성능을 올리기 위해 할 수 있는 설정들이 있다.
가령 변경 속도에 대해서는 캐싱을 하거나, 워크로드 개수에 대해서는 영향을 받을 워크로드를 명확히 지정하는 방식이 가능하다.
컨트롤 플레인 메트릭
그렇다면 여기에서 주의 깊게 볼 메트릭은 무엇인가?
황금 신호에 기반해서 메트릭을 나누어서 살펴보자.
지연 시간
pilot_proxy_convergence_time
- 대기열에 들어간 푸시 요청이 워크로드에 적용되는 시간pilot_proxy_queue_time
- 대기열에 머무른 시간pilot_xds_push_time
- 설정이 푸시되는데 들어간 시간- 전송 데이터양이 영향을 미치기에 네트워크 대역폭의 영향을 받는다.
- 설정 업데이트 크기, 대상 워크로드 개수를 최적화하여 개선할 수 있다.
스터디에서 나온 내용 상으로는 훨씬 에쁘게 보였는데, 최신 버전의 문제인 건지 모르겠지만 내 코드에서는 히트맵으로 설정된 채 보인다.
sum(rate(pilot_xds_push_time_bucket{}[1m])) by (le)
직접 쿼리를 조금 짜서 봤다.
하나의 선으로 보이지만 모든 선이 똑같은 값을 가지고 있어서 그런 것이다.
그라파나 쪽에선 잘 모르겠지만 프로메테우스로 보면 stacked를 체크해서 다 같은 값이어도 겹쳐있는 것을 조금 더 확실하게 확인할 수 있다.
포화도
리소스 사용량으로 보면 된다.
sum (irate(container_cpu_usage_seconds_total{container="discovery",pod=~"istiod-.*"}[1m])) by (pod)
istiod에서 예민한 자원은 cpu로 cpu의 사용량을 기준으로 보면 된다.
노드 차원에서 수집하여 볼 수 있는 통계로는 위처럼 볼 수 있는데, istiod 자체가 메트릭을 노출하고 있으므로 다음의 쿼리를 이용하는 것도 방법이다.
irate(process_cpu_seconds_total{app="istiod"}[1m])
이스티오의 자원 사용량은 특정 타이밍마다 증가하는데, 서비스나 생길 때 관련한 설정들을 갑자기 만들어서 전달해야 하기 때문에 그렇다.
평소에는 idle 상태를 유지하다가 특정 타이밍마다 많이 생기게 되기 때문에 적절한 스케일링 전략을 고려하는 것이 도움이 될 것이다.
가령 배포가 많이 일어날 시기에는 미리 스케일링을 하는 식으로 조절할 수 있다.
전체적으로 메트릭을 보면 특정 순간에만 치고 올라오는 현상을 많이 보게 될 것이다.
이는 설정이 들어갈 때만 연산량이 많아지기 때문에 발생하는 현상으로, 보통은 이게 자연스러운 상황이다.
트래픽
그렇다면 단위 요청 수는 얼마나 되는가?
이스티오의 컨트롤 플레인의 트래픽 단위는 수신받는 이벤트, 푸시하는 설정을 들 수 있겠다.
각각의 지점이 병목 포인트가 될 수 있으며 저마다 최적화할 전략이 있기 때문에 둘 다 염두해둘 필요가 있다.
여기에 대해 그라파나 시각화가 전부 다 돼있진 않지만, 메트릭으로는 여러 개 볼 수 있는 지점이 있다.
수신 관련
pilot_inbound_updates
istiod 인스턴스마다 업데이트를 수신한 횟수 카운터이다.
실제로 더 많은 이벤트나 요청이 있어도, 실제로 변경을 수행한 것은 이만큼이다.
pilot_push_triggers
그렇다면 푸시를 트리거한 그러한 이벤트들이 몇 번이나 있었는가 하면 이 메트릭을 보면 된다.
발신 관련
pilot_xds_pushes
실제로 엔보이에 적용한 모든 종류의 XDS api 횟수를 말한다.
sum by (type) (irate(pilot_xds_pushes[$__rate_interval]))
순간 ops/s
라고 써져있어서 자연스럽게 속도를 나타내는 건가 싶었는데, 위 그래프는 얼마나 자주 푸시가 일어나는지를 담고 있다.
초당 푸시 수 정도로 보면 되겠다.
pilot_xds
이건 동기화를 할 대상들의 수를 말한다.
정확하게는 커넥션 수를 말한다.
pilot_xds_config_size_bytes_bucket
xds 설정 파일 크기를 나타낸 메트릭.
이건 설정 별로 크기가 얼마나 되는지 나타낸다.
envoy_cluster_upstream_cx_tx_bytes_total, envoy_cluster_upstream_cx_rx_bytes_total을 통해서도 비슷한 정보를 볼 수 있다.
이것들은 기본적으로는 엔보이에서 노출하는 메트릭 정보로, 각 엔보이가 컨트롤 플레인과 통신하며 주고 받은 설정 파일 크기, 동기화 트래픽 등을 나타낸다.
keti debug -- curl localhost:15020/metrics | grep envoy_cluster
각 엔보이에 쿼리를 날려서 확인해볼 수 있다.
엔보이가 노출하는 메트릭 정보는 굉장히 많은데,[1] 이스티오는 이중에서 극히 일부를 기본 메트릭으로 노출한다.[2]
위 cluster.xds-grpc
키가 프로메테우스 메트릭 상으로는 라벨로 들어간다.
기타
pilot_services, pilot_virt_services
이스티오에서 추적하고 있는 버츄얼 서비스, 서비스 개수를 나타낸다.
서비스 개수도 클러스터 내에 있는 것만큼 다 가지고 있는데, 이는 컨트롤 플레인이 제멋대로 모든 네임스페이스를 추적하고 있다는 말과 같다!
불필요한 리소스를 추적한다는 건 엔보이에 적용할 설정 파일의 크기를 불필요하게 늘리게 되는 상황을 야기하기에 이를 개선할 필요가 있다.
오류
istiod가 포화 상태에 이르렀을 때 커넥션 수가 넘치거나 큐가 지나치게 쌓이며 에러가 발생할 수 있다.
(현재는 간단한 테스트만 하느라 에러가 발생할 일이 없..)
이와 관련된 주요한 메트릭들은 이 정도가 있다.
- pilot_xds_rejects - 설정 푸시 거부 횟수
- pilot_xds_push_context_errors - 푸시하다 에러 난 횟수
- pilot_xds_write_timeout - 설정 변환에서 타임아웃 난 횟수
- pilot_total_xds_internal_errors - 에러 총합
이 정보들은 어떤 설정이 많이 거부되는지, 어떤 부분이 취약하여 향후 어떤 장애로 이어질지 예측하는데 도움을 준다.
가령 CDS 설정에서 에러가 많이 발생하는 걸 포착해 서비스 레지스트리 최적화가 필요하다고 짐작해볼 수 있다.
그런데 istiod에 에러가 발생한다는 것 자체가 사실 심각한 문제라, 기왕이면 에러가 발생하기 이전에 다른 지표들을 통해 발생할 병목 요인을 탐색하는 게 베스트일 거라 본다.
아래에서 보겠지만, istiod는 포화 상태가 되더라도 이를 즉각적으로 대응하는데 살짝 제약 사항이 있다.
그러므로 최대한 에러가 발생하지 않도록, 포화도를 미리 선제적으로 잘 관측할 필요가 있다.
성능 개선
컨트롤 플레인의 성능에 영향을 주는 요소 4가지를 개선하고자 한다면, 다음의 방법들을 사용할 수 있다.
- 변경 속도 - 변경 속도가 빠를수록 데이터 플레인을 동기화하기 위해 더 많이 처리
- 서비스 메시와 관련 없는 이벤트 무시
- 이벤트 배치 처리 기간을 데이터 플레인 업데이트에 필요한 푸시 횟수 줄이기
- 할당된 리소스 - 프로세스는 당연히 컴퓨팅 자원에 영향 받음..
- 스케일 아웃으로 파일럿 인스턴스들이 관리할 워크로드 분할
- 스케일 업으로 엔보이 설정 생성 속도 높이고 푸시 동시 처리량 높이기
- 업데이트할 워크로드 개수 - 업데이트할 워크로드가 많을 수록 네트워크 대역폭과 처리 능력 필요
- Istio Sidecar 리소스를 통해 설정에 영향 받을 워크로드 범위 지정
- 설정 크기 - 엔보이 설정 파일이 클 수록 처리 능력과 네트워크 대역폭이 필요
- Istio Sidecar 리소스를 통해 워크로드가 필요한 설정만 받도록 설정
사이드카 리소스는 성능 최적화의 핵심이 되는 리소스라 할 수 있다.
다른 리소스들의 설정 방식도 다 그렇지만 결국 어떤 범위에 해당 설정이 적용되게 할지가 관건이 된다.
사이드카 리소스는 아예 핵심적으로 이것만 전담하는 리소스로서 메시 전체의 부하를 줄이는 효과도 있다.
사이드카 자체의 레이턴시는 동시 커넥션 수에 따라 확장된다.[3]
세팅 - 스크립트로 부하 테스트
일부러 부하가 걸릴 상황을 만들 것이다.
kubectl -n istioinaction apply -f services/catalog/kubernetes/catalog.yaml
kubectl -n istioinaction apply -f ch11/catalog-virtualservice.yaml
kubectl -n istioinaction apply -f ch11/catalog-gateway.yaml
kubectl -n istioinaction apply -f ch11/sleep-dummy-workloads.yaml
while true; do curl -K curl.conf -s http://catalog.istioinaction.io:30000/items ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; echo; done
while true; do kubectl top pod -n istio-system -l app=istiod --containers=true ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; echo; done
일단 기본적으로 많은 개수의 워크로드를 배치한다.
사실 끽해봐야 10개 남짓한 개수만 배포했는데, 실제 워크로드를 배치하지 않아도 서비스를 늘리는 것만으로 쉽게 부하를 줄 수 있다.
각 서비스에 대해 엔보이들에 설정을 반영해줘야 하기 때문이다.
이 파일을 적용해보자!
서비스와 게이트웨이, 버추얼 리소스를 무려 200개씩 때려박는 무식한 양식 파일이다.
kubectl -n istioinaction apply -f ch11/resources-600.yaml
순간 cpu, 메모리 사용량이 잠시 치솟는 것을 볼 수 있다.
그라파나 메트릭으로 확인해도 마찬가지이다.
rate(pilot_xds_pushes[$__rate_interval])
간단하게 설정을 보면 eds, 즉 엔드포인트 설정 횟수가 어마무시하게 많은 것을 볼 수 있다.
그러나 이 정도는 거뜬하다는 듯, 시간에는 거의 차이가 발생하지 않는 것을 볼 수 있다.
처리하는 요청 개수 자체는 늘었으나 시간은 전부 다 0.01 아래로 동일했다는 것이다.
여기에 본격적으로 부하 테스트를 진행할 것이다.
책에서 일부러 여러 서비스를 만들었다 지웠다하면서 컨트롤 플레인에 부하를 주는 스크립트를 제공한다.
#!/bin/bash
main(){
## Pass input args for initialization
init_args "$@"
SLEEP_POD=$(kubectl -n istioinaction get pod -l app=sleep -o jsonpath={.items..metadata.name} -n istioinaction | cut -d ' ' -f 1)
PRE_PUSHES=$(kubectl exec -n istio-system deploy/istiod -- curl -s localhost:15014/metrics | grep pilot_xds_pushes | awk '{total += $2} END {print total}')
if [[ -z "$PRE_PUSHES" ]]; then
echo "Failed to query Pilot Pushes from prometheus."
echo "Have you installed prometheus as shown in chapter 7?"
exit 1
fi
echo "Pre Pushes: $PRE_PUSHES"
INDEX="0"
while [[ $INDEX -lt $REPS ]]; do
SERVICE_NAME="service-`openssl rand -hex 2`-$INDEX"
create_random_resource $SERVICE_NAME &
sleep $DELAY
INDEX=$[$INDEX+1]
done
## Wait until the last item is distributed
while [[ "$(curl --max-time .5 -s -o /dev/null -H "Host: $SERVICE_NAME.istioinaction.io" -w ''%{http_code}'' $GATEWAY/items)" != "200" ]]; do
# curl --max-time .5 -s -o /dev/null -H "Host: $SERVICE_NAME.istioinaction.io" $GATEWAY/items
sleep .2
done
echo ==============
sleep 10
POST_PUSHES=$(kubectl exec -n istio-system deploy/istiod -- curl -s localhost:15014/metrics | grep pilot_xds_pushes | awk '{total += $2} END {print total}')
echo
LATENCY=$(kubectl -n istioinaction exec -it $SLEEP_POD -c sleep -- curl "$PROM_URL/api/v1/query" --data-urlencode "query=histogram_quantile(0.99, sum(rate(pilot_proxy_convergence_time_bucket[1m])) by (le))" | jq '.. |."value"? | select(. != null) | .[1]' -r)
echo "Push count:" `expr $POST_PUSHES - $PRE_PUSHES`
echo "Latency in the last minute: `printf "%.2f\n" $LATENCY` seconds"
}
create_random_resource() {
SERVICE_NAME=$1
cat <<EOF | kubectl apply -f -
---
kind: Gateway
apiVersion: networking.istio.io/v1alpha3
metadata:
name: $SERVICE_NAME
namespace: $NAMESPACE
spec:
servers:
- hosts:
- "$SERVICE_NAME.istioinaction.io"
port:
name: http
number: 80
protocol: HTTP
selector:
istio: ingressgateway
---
apiVersion: v1
kind: Service
metadata:
labels:
app: catalog
name: $SERVICE_NAME
namespace: $NAMESPACE
spec:
ports:
- name: http
port: 80
protocol: TCP
targetPort: 3000
selector:
app: catalog
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: $SERVICE_NAME
namespace: $NAMESPACE
spec:
hosts:
- "$SERVICE_NAME.istioinaction.io"
gateways:
- "$SERVICE_NAME"
http:
- route:
- destination:
host: $SERVICE_NAME.istioinaction.svc.cluster.local
port:
number: 80
---
EOF
}
help() {
cat <<EOF
Poor Man's Performance Test creates Services, Gateways and VirtualServices and measures Latency and Push Count needed to distribute the updates to the data plane.
--reps The number of services that will be created. E.g. --reps 20 creates services [0..19]. Default '20'
--delay The time to wait prior to proceeding with another repetition. Default '0'
--gateway URL of the ingress gateway. Defaults to 'localhost'
--namespace Namespace in which to create the resources. Default 'istioinaction'
--prom-url Prometheus URL to query metrics. Defaults to 'prom-kube-prometheus-stack-prometheus.prometheus:9090'
EOF
exit 1
}
init_args() {
while [[ $# -gt 0 ]]; do
case ${1} in
--reps)
REPS="$2"
shift
;;
--delay)
DELAY="$2"
shift
;;
--gateway)
GATEWAY="$2"
shift
;;
--namespace)
NAMESPACE="$2"
shift
;;
--prom-url)
PROM_URL="$2"
shift
;;
*)
help
;;
esac
shift
done
[ -z "${REPS}" ] && REPS="20"
[ -z "${DELAY}" ] && DELAY=0
[ -z "${GATEWAY}" ] && GATEWAY=localhost:30000
[ -z "${NAMESPACE}" ] && NAMESPACE=istioinaction
[ -z "${PROM_URL}" ] && PROM_URL="prometheus.istio-system.svc.cluster.local:9090"
}
main "$@"
원래 있던 코드에서 맨 아래 GATEWAY
, PROM_URL
만 현재 내 세팅에 맞게 수정해주었다.
이건 xDS Push수 증가량과 프록시 구성 수렴 시간(latency)을 확인한다.
이를 통해 최종적으로 Push 성능과 latency를 평가할 수 있다.
./performance-test.sh --reps 10 --delay 2.5
해당 파일에 실행권한을 준 뒤에 실행해본다.
10개의 리소스를 만들고, 만들 때 2.5의 간격을 두도록 설정했다.
흐음.. 내 컴이 생각보다 성능이 좋나..
./performance-test.sh --reps 10
지연을 없애고 해도 성능이 좋은 것 같다..
리소스를 만든 횟수는 똑같은데 푸시 횟수가 줄어든 것은 디바운스 때문인 것으로 보인다.
sum(rate(pilot_debounce_time_bucket[1m])) by (le)
간단하게 디바운싱되느라 큐에 들어가지 못하고 걸린 시간을 측정해볼 수 있는데, 1초 이상의 시간이 걸린 것들이 더러 생겼다.
원하는 결과가 안 나오는 것 같아서 delay없이 100개 때려박아봤는데, 푸시에 0.1초 가까이 걸리는 것들이 생겨나기 시작했다.
이해하고 나니까 히트맵이 굉장히 보기 좋은 것 같다
푸시된 설정 파일 사이즈 역시 조금씩 증가하는 것이 확인된다.
그런데 역시 아무리 봐도 생각한 수준의 부하가 측정되질 않는 것 같다..?
이 스크립트는 만든 리소스를 삭제하지 않는다..
그래서 마구 걸어댔더니 서비스랑 버추얼 서비스 개수가 1000개를 넘어버렸다 ㅋㅋ
기어코 cidr 범위가 꽉찼다는 에러까지 나온다...
최근 기본 리소스가 된 ServiceCIDR을 써보려했는데 Kubernetes v1.33 - Octarine에 기본으로 활성화되어서 현 클러스터에 설정할 수 없다..
apiVersion: networking.k8s.io/v1
kind: ServiceCIDR
metadata:
name: new-cidr
spec:
cidrs:
- 10.201.0.0/22
그렇다면 바로 버전 업.
어차피 추가 실습을 이어나가야 하는 마당에 cidr이 꽉찬 상황을 해결해야 했기에 잘됐다.
그런데 아무리 해봐도 레이턴시가 증가하질 않는다..
kubectl -n istioinaction exec -it sleep-5f6f494dc8-2l9kd -c sleep -- curl "prometheus.istio-system.svc.cluster.local:9090/api/v1/query" --data-urlencode "query=histogram_quantile(0.99, sum(rate(pilot_proxy_convergence_time_bucket[1m])) by (le))" | jq '.. |."value"? | select(. != null) | .[1]' -r
쿼리문을 직접 실행해서 확인해봐도 확실히 그렇고, 프로메테우스로 확인해도 마찬가지이다.
거의 변동이 없어서 뭔가 유의미한 확인을 하는 것이 어렵다.
Sidecar 리소스 활용
유의미한 가시화는 안 되지만, 일단 책에서 나온 실습 그대로 진행하는 걸로 정했다.
기본적으로는 모든 서비스가 메시의 모든 워크로드를 알도록 설정된다.
사이드카가 특히 유용할 수밖에 없는 상황은 동서 트래픽을 설정할 때이다.
버츄얼 서비스로 메시 환경 내의 트래픽을 처리하려면 진입점에 mesh
를 적어줘야 하는데, 이건 기본적으로 메시 전체 서비스가 해당 버추얼 서비스를 인식하게 만든다.
대충 설정했다간 괜한 버츄얼 서비스 하나에 모든 프록시가 업데이트를 받아야 하는 상황이 생긴다는 것.
CATALOG_POD=$(kubectl -n istioinaction get pod -l app=catalog -o jsonpath={.items..metadata.name} | cut -d ' ' -f 1)
kubectl -n istioinaction exec -ti $CATALOG_POD -c catalog -- curl -s localhost:15000/config_dump > /tmp/config_dump
du -sh /tmp/config_dump
서비스가 증가할수록 설정 파일의 크기도 증가하는 것은 위에서 확인됐다.
현재 단일 엔보이에 들어간 설정 파일의 크기는 무려 2.4메가..
이제 이를 해소하기 위해 사이드카 리소스를 적용해본다.
apiVersion: networking.istio.io/v1beta1
kind: Sidecar
metadata:
name: default
namespace: istioinaction
spec:
workloadSelector:
labels:
app: foo
egress:
-hosts:
- "./bar.istioinaction.svc.cluster.local"
- "istio-system/*"
outboundTrafficPolicy:
mode: REGISTRY_ONLY
워크로드를 고르고, 이 워크로드에 드나드는 트래픽 설정을 명확하게 제한을 건다.
인그레스 설정하면 이쪽으로 향할 호스트 방식 지정
이그레스는 해당 워크로드가 대상으로 삼을 수 있는 서비스 지정.
디폴트로서 추천하는 건 모든 프록시의 트래픽 송신에 대해 istio-system의 서비스로만 가도록 설정하는 것.
모든 프록시는 컨트롤 플레인에 연결되고 다른 서비스 간 연결은 삭제된다.
이 상태에서 프록시 간 연결이 필요한 것들에 대해 명시적으로 지정하며 네트워크 경로를 지정하면 설정을 최소화시킬 수 있다.
apiVersion: networking.istio.io/v1beta1
kind: Sidecar
metadata:
name: default # istio-system 네임스페이스의 사이드카는 메시 전체에 적용된다.
namespace: istio-system # 위 설명 동일.
spec:
egress:
- hosts:
- "istio-system/*" # istio-system 네임스페이스의 워크로드만 트래픽 송신을 할 수 있게 설정한다.
- "prometheus/*" # 프로메테우스 네임스페이스도 트래픽 송신을 할 수 있게 설정한다.
outboundTrafficPolicy:
mode: REGISTRY_ONLY # 모드는 사이드카에 설정한 서비스로만 트래픽 송신을 허용한다
이게 그 설정이다.
CATALOG_POD=$(kubectl -n istioinaction get pod -l app=catalog -o jsonpath={.items..metadata.name} | cut -d ' ' -f 1)
kubectl -n istioinaction exec -ti $CATALOG_POD -c catalog -- curl -s localhost:15000/config_dump > /tmp/config_dump
du -sh /tmp/config_dump
이걸 적용하자마자 설정 파일의 크기가 4배는 줄어들었다.
그렇다면 과연 컨트롤 플레인 부하는 줄어들었을까?
./performance-test.sh --reps 15 --delay 0.5
일단 푸시 개수는 줄어든 것이 확인된다.
그라파나로도 cpu 사용량 등이 확실하게 줄은 것은 확인할 수 있다.
이벤트를 무시할 네임스페이스 지정
이스티오는 모든 네임스페이스의 모든 리소스에 대해 추적한다..
그래서 네임스페이스 셀렉터 설정을 넣어서 범위를 제한하자.
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
metadata:
namespace: istio-system
spec:
meshConfig:
discoverySelectors: # 디스커버리 셀렉터 활성화
- matchExpressions:
- key: istio-exclude
operator: NotIn
values:
- "true"
이스티오 오퍼레이터 양식 파일에서 추적할 네임스페이스를 설정했다.
exclude 라벨에 true라 적히지 않은 네임스페이스만 추적하는 설정이다.
kubectl create ns new-ns2
kubectl label namespace new-ns2 istio-injection=enabled
cat << EOF | kubectl apply -n new-ns2 -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP
EOF
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/istio-ingressgateway.istio-system | grep nginx | wc -l
kubectl label ns new-ns2 istio-exclude=true
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/istio-ingressgateway.istio-system | grep nginx | wc -l
생각지 못한 상황 발생..!
게이트웨이 리소스 최적화를 한답시고 처음부터 게이트웨이에 필터링 설정을 넣었던 탓인지, 기본적으로 클러스터를 인식하지 않고 있다!
이게 원인이라 생각해서 없애고 게이트웨이도 재시작시켜줬으나, 결과는 실패.
원인을 잘 모르겠는데, 이건 다음에 조금 더 파봐야겠다.
동기화 작업 배치 처리
디바운스 기간을 조금 더 넓게 주는 것도 성능 최적화의 하나의 방법이다.
배치 간격이 넓어지므로 자원을 조금 더 효율적으로 사용하여 이벤트를 처리할 수 있게 된다.
3번에 걸쳐서 설정될 예정이었던 이벤트가 한번에 적용돼서 한번에 푸시만으로 해결된다면 속도 향상을 꾀할 수 있게 된다.
(물론 설정들이 병합되며 개별 설정 파일 크기가 증가할 수는 있다.)
그러나 기간이 넓어질수록 설정이 빠르게 적용되지 못하여 지연이 발생할 수 있으니, 적절한 균형 지점을 찾아야할 것이다.
관련한 istiod 프로세스의 환경 변수는 다음의 것들이 있다.[4]
- PILOT_DEBOUNCE_AFTER(기본 100ms)
- 이벤트를 큐에 넣기 전 디바운스할 시간을 지정
- sliding window 간격을 정한다고 보면 된다.
- PILOT_DEBOUNCE_MAX(기본 10초)
- 디바운스 허용할 최대 시간
- 이 시간이 지나면 현재 병합된 이벤트가 푸시 대기열에 추가된다.
- PILOT_ENABLE_EDS_DEBOUNCE(기본 true)
- 엔드포인트 업데이트가 디바운스 규칙을 준수할지, 큐에 즉시 배치할지 지정
- PILOT_PUSH_THROTTLE(기본 100개 동시 푸시)
- 동시에 처리하는 푸시 요청 개수 지정
해당 설정이 제대로 적용되는지 확인만 하기 위해 디바운스로 활용할 시간을 엄청 넓게 줘보자.
values:
pilot:
env:
ENABLE_NATIVE_SIDECARS: true
PILOT_DEBOUNCE_AFTER: "2500ms"
이번에도 이스티오 오퍼레이터 양식 파일에서 설정하면 된다.(기본값은 100ms == 0.1초)
istiod 양식 환경변수에 해당 값이 들어가면 성공
./performance-test.sh --reps 15 --delay 0.5
푸시횟수가 말도 안 되게 줄어들었다 ㅋㅋ
분명 푸시 횟수가 줄어들어서 그런지, 컴퓨팅 자원 사용량아 어마무시하게 낮아진 것을 확인할 수 있다.
(설정이 다시 되다보니 istiod가 재가동되면서 다른 파드로 찍힌다.)
메모리 할당량이 어마무시하게 줄어든 게 조금 놀랍다.
고루틴은 아예 새로 파드가 띄워지면서 변동이 크게 일어났으므로 신뢰하기 힘든 지표라고 생각한다.
어쩌면 istiod가 오래 기동되면서 적절히 작업을 마치지 못한 고루틴의 수가 늘어나고 있던 게 아닐까 싶긴 하다.
그러나 결과적으로는 설정이 디바운스되도록 기다리게 되는 양이 늘어났을 것을 감안해야 한다.
이 값은 흔히 보게 되는 convergence_time 메트릭에는 드러나지 않지만, 궁극적으로 설정에 딜레이를 부여한다는 점은 두말할 여지가 없다.
디바운스가 업데이트 전파에 끼친 영향에 대해 언급한 다른 글도 있다.[5]
그렇다면 설정을 어떻게 하는 게 좋을까?
워크로드 스케일링 등의 이슈로 발생할 수 있는 팬텀 워크로드 현상은 엔드포인트 설정이 즉각 반영되지 않기 때문에 나타나는 문제이다.
그러므로 PILOT_ENABLE_EDS_DEBOUNCE
를 false로 둬 엔드포인트는 즉각적인 업데이트가 되도록 보장해주는 편이 좋다.
리스너, 라우터 등의 설정은 대체로 사람이 설정한 리소스가 반영될 때 수정된다.
이런 것들은 정해진 배포 시간이나, 최소한 사람이 인지하고 있는 상태에서 변경이 일어나기 때문에 상대적으로 느리더라도 상관 없는 경우가 많다.
그러나 클러스터 환경을 반영하는 업데이트는 늦춰지면 곧 서비스 장애로 직결되기에 이런 부분들은 주의할 필요가 있다.
리소스 추가할당
위의 방법들을 전부 시도하고도 성능을 더 향상시켜야 한다면, 남은 방법은 아예 컨트롤 플레인에 컴퓨팅 자원을 더 할당해주는 것.
책에서는 병목 지점이 어디냐에 따라 스케일링 전략을 다르게 가져가야 한다고 말한다.
- 송신 트래픽은 스케일 아웃 - 각 istiod가 관리하는 워크로드 수를 줄여야 하기 때문.
- 수신 트래픽은 스케일 업 - 이스티오 리소스 설정을 많이 처리해야 하기 때문.
istioctl install --set profile=demo \
--set values.pilot.resources.requests.cpu=1000m \
--set values.pilot.resources.requests.memory=1Gi \
--set values.pilot.replicaCount=2 -y
오토스케일링은 좋은 전략이지만, 여기에 유념할 것이 있다.
istiod가 확장되더라도 기존에 커넥션을 유지하는 데이터플레인은 그대로 커넥션을 계속 유지한다는 것이다.
부트스트랩되어 설정된 ADS api 통로는 커넥션이 연결된 30분 간은 기본으로 연결되도록 되어 있다.
이를 수정하려면 MaxServerConnectionAge를 만져야 한다.
아무튼 이것 때문에 기존 istiod 파드에 부하가 걸려 스케일링이 알아서 되더라도 막상 새 파드로는 트래픽이 연결되지 않을 수 있다.
그럼 새 파드에는 부하가 발생하지 않고, 이로 인해 의도되지 않게 메트릭이 수집되지 않아 결국 다시 다운스케일이 돼버리는, 플래핑(flapping) 현상이 발생할 수 있다.
그래서 오토스케일링을 할 때 이 점을 유념할 필요가 있다.
대체로 컨트롤 플레인의 오토스케일링은 순간적인 부하를 대응하는 것보다는 전반적인 부하량을 바탕으로 설정하는 것이 기본이다.
그런데 이럴 거라면 사실 오토스케일링을 한다는 메리트가 그다지 있진 않은 것 같다.
결론
컨트롤 플레인 성능을 개선하기 위해 시도할 수 있는 방법 순서는 이 정도로 보면 좋다.
- 사이드카 설정 - 이것만으로도 대부분 이득 본다.
- 컨트롤 플레인이 포화 상태인데 이미 리소스를 많이 할당한 경우에는 이벤트 배치 수정
- 병목이 송신 트래픽일 때 istiod 스케일 아웃
- 병목이 수신 트래픽일 때 istiod 스케일 업
다음으로 성능을 최적화하는데 있어서 참고하면 좋을 지침을 보자면,
- 성능 문제인지 확인
- 데이터 플레인에서 컨트롤 플레인으로 연결이 제대로 이뤄지고 있는가?
- 플랫폼 문제인가? 이를테면 쿠버네티스에서 API 서버가 정상인가?
- 변경 범위를 지정하도록 Sidecar 리소스를 정의했는가?
- 성능 병목 지점 파악
- 황금 신호 기반 파악
- 컨트롤 플레인이 포화 상태도 아닌데 지연 시간이 증가하면 리소스가 최적으로 활용되지 않고 있다는 것
- 더 많은 푸시가 동시에 처리되도록 동시 푸시 임계값을 늘릴 수 있다.
- 사용률은 낮지만 부하가 걸렸을 때 빠르게 포화 상태가 되면 변경 사항이 매우 폭발적임
- 즉, 변경 사항이 없는 기간이 길다가 짧은 시간에 이벤트가 급증하는 것이다.
- 이스티오 파일럿의 복제본 수를 늘리거나, 업데이트를 미룰 여지가 있는 경우 배치 속성을 조정한다.
- 점진적 변경
- 성능 최적화에는 트레이드오프가 있는 경우가 많다.
- 그러므로 점진적으로 변화되는 지표를 확인해가면 변경을 하는 것이 좋다.
- 안전성
- istiod의 에러는 메시 장애로 이어진다.
- 리소스는 되도록 관대하게, 고가용성 구성을 해두자.
- 버스트 가능한 자원 고려
- istiod는 순간적인 cpu 리소스가 필요하기에 항상 고성능이기보다는 버스트가 가능한 자원을 사용하는 것이 비용효율
성능 문제인지 먼저 확인하라는 건 이스티오가 기본적으로 리소스를 적게 활용하면서 높은 부하를 견딜 수 있기 때문이다.
생각 이상으로 정말 잘 견디는 듯
이스티오는 새 버전이 나올 때 항상 성능 테스트를 진행하고 결과를 공개하고 있다.[6]
- 엔보이 설정을 부풀리는 쿠버네티스 서비스 1,000개
- 동기화해야 하는 워크로드 2,000개
- 서비스 메시 전체에서 초당 요청 70,000개
istiod는 이 정도의 부하를 견디는데 겨우 vcpu 하나에 메모리 1.5기가면 족하다!
그러니 성능이 저하된다면 일단 istiod를 뜯어고치는 것보다는 다름 부분에서 최적활할 부분이 있는지 찾아보는 것이 도움이 된다.
여담으로 1kb짜리 1000개의 http 요청을 보낼 때 이스티오의 프록시들이 요구하는 자원에 대한 내용도 있는데,
- 2개의 워커 쓰레드를 가진 사이드카 - 0.20 vCPU, 60 MB 메모리
- 2개의 워커 쓰레드를 가진 웨이포인트 - 0.25 vCPU, 60 MB 메모리
- ztunnel - 0.06 vCPU, 12 MB 메모리
ztunnel이 가장 성능은 좋긴 한데, 기본 사이드카 역시 굉장히 작은 자원을 사용하는 것을 볼 수 있다.
관련 문서
지식 문서, EXPLAIN
이름4 | is-folder | 생성 일자 |
---|---|---|
E-이스티오 컨트롤 플레인 성능 최적화 | false | 2025-05-18 02:29 |
E-이스티오 컨트롤 플레인 메트릭 | false | 2025-05-18 15:45 |
istiod | false | 2025-05-18 03:16 |
Istio Sidecar | false | 2025-05-13 22:27 |
기타 문서
Z0-연관 knowledge, Z1-트러블슈팅 Z2-디자인,설계, Z3-임시, Z5-프로젝트,아카이브, Z8,9-미분류,미완이름0 | 코드 | 타입 | 생성 일자 |
---|
참고
https://www.envoyproxy.io/docs/envoy/latest/configuration/upstream/cluster_manager/cluster_stats#general ↩︎
https://istio.io/latest/docs/ops/configuration/telemetry/envoy-stats/ ↩︎
https://istio.io/latest/blog/2019/performance-best-practices/ ↩︎
https://istio.io/latest/docs/reference/commands/pilot-discovery/#envvars ↩︎
https://medium.com/airbnb-engineering/improving-istio-propagation-delay-d4da9b5b9f90 ↩︎
https://istio.io/latest/docs/ops/deployment/performance-and-scalability/ ↩︎